<?php

namespace App\Handlers\Commands;

use App\Commands\EditVulnerability as EditVulnerabilityCommand;
use App\Entities\Vulnerability;
use App\Exceptions\ActionNotPermittedException;
use App\Exceptions\InvalidInputException;
use App\Exceptions\UserNotFoundException;
use App\Exceptions\VulnerabilityNotFoundException;
use App\Exceptions\WorkspaceNotFoundException;
use App\Policies\ComponentPolicy;
use App\Repositories\AssetRepository;
use App\Repositories\VulnerabilityRepository;
use App\Repositories\WorkspaceAppRepository;
use App\Services\ImageService;
use Doctrine\ORM\EntityManager;
use Exception;

class EditVulnerability extends AbstractVulnerabilityHandler
{
    /**
     * EditVulnerability constructor.
     *
     * @param AssetRepository $assetRepository
     * @param VulnerabilityRepository $vulnerabilityRepository
     * @param WorkspaceAppRepository $workspaceAppRepository
     * @param EntityManager $em
     * @param ImageService $service
     */
    public function __construct(
        AssetRepository $assetRepository, VulnerabilityRepository $vulnerabilityRepository,
        WorkspaceAppRepository $workspaceAppRepository, EntityManager $em, ImageService $service
    )
    {
        parent::__construct($assetRepository, $vulnerabilityRepository, $workspaceAppRepository, $em, $service);
    }

    /**
     * Process the EditVulnerability command.
     *
     * @param EditVulnerabilityCommand $command
     * @return Vulnerability
     * @throws InvalidInputException
     * @throws UserNotFoundException
     * @throws WorkspaceNotFoundException
     * @throws Exception
     */
    public function handle(EditVulnerabilityCommand $command)
    {
        // Get the authenticated User
        $requestingUser = $this->authenticate();

        $vulnerabilityId      = $command->getId();
        $assetIds             = $command->getAssetIds();
        $vulnerabilityDetails = $command->getRequestedChanges();
        // Check that all the required fields were set on the command
        if (!isset($vulnerabilityId, $assetIds, $vulnerabilityDetails)) {
            throw new InvalidInputException("One or more required members are not set on the command");
        }

        /** @var Vulnerability $vulnerability */
        $vulnerability = $this->vulnerabilityRepository->find($vulnerabilityId);
        // Make sure the WorkspaceApp exists
        if (empty($vulnerability)) {
            throw new VulnerabilityNotFoundException(
                "A Vulnerability with the given ID does not exist."
            );
        }

        // Make sure we are editing a Vulnerability on a Ruggedy ScannerApp. Currently this is the only place where
        // editing Vulnerabilities is enabled
        if (!$vulnerability->getFile()->getWorkspaceApp()->isRuggedyApp()) {
            throw new ActionNotPermittedException(
                "Cannot edit Vulnerabilities on any WorkspaceApp other than the Ruggedy WorkspaceApp"
            );
        }

        // Make sure the User has permission to edit a Vulnerability on a WorkspaceApp
        if ($requestingUser->cannot(ComponentPolicy::ACTION_UPDATE, $vulnerability)) {
            throw new ActionNotPermittedException(
                "The requesting User does not have permission to edit Vulnerabilities"
            );
        }

        // Populate the Vulnerability with data from the request and explicitly set the thumbnail fields because null
        // values are ignored by setFromArray and we may be unsetting a thumbnail image.
        $vulnerability->setFromArray($vulnerabilityDetails);

        $this->handleUpdatedThumbnails($vulnerability, $vulnerabilityDetails);
        $this->doVulnerabilityPostProcessing($vulnerability, $assetIds);

        // Save all changes to the DB and refresh the Vulnerability with the changes
        $this->em->flush();
        $this->em->refresh($vulnerability);

        return $vulnerability;
    }

    /**
     * Delete Thumbnails that have been removed and update thumbnail properties where necessary
     *
     * @param Vulnerability $vulnerability
     * @param array $vulnerabilityDetails
     */
    protected function handleUpdatedThumbnails(Vulnerability $vulnerability, array $vulnerabilityDetails)
    {
        // Convert the array to a Collection for ease of use
        $vulnerabilityDetails = collect($vulnerabilityDetails);

        // Iterate over the Vulnerability property to getter map
        $vulnerability->getThumbnailPropertyGetterMap()
            ->filter(function ($getter, $property) use ($vulnerability) {
                // Make sure there is a mapped setter method for the property
                return $vulnerability->getThumbnailPropertySetterMap()->get($property) !== null;
            })
            ->each(function ($getter, $property) use ($vulnerability, $vulnerabilityDetails) {
                // Get the setter
                $setter = $vulnerability->getThumbnailPropertySetterMap()->get($property);
                if (!empty($vulnerability->$getter()) && empty($vulnerabilityDetails->get($property))) {
                    // Thumbnail value was set but is now empty, remove the thumbnail from storage and
                    // set the entity property value to null
                    $this->service->removeVulnerabilityThumbnail($vulnerability->$getter());
                    return $vulnerability->$setter(null);
                }

                // The value from the request is null so there are no changes to be made
                if ($vulnerabilityDetails->get($property) === null) {
                    return null;
                }

                // Set the value of the thumbnail to the new value from the request
                return $vulnerability->$setter($vulnerabilityDetails->get($property));
            });
    }
}